有些問題需要在其他不相關的物件之間共用行為。這種共同行為對類別來說是正確的 ,它是物件所扮演的角色。
在設計物件導向程式時,有些問題需要多個不相關的物件之間共享相似的行為,這種共同行為被視為角色,物件可以扮演這些角色。
當不相關的物件開始扮演相同的角色時,它們建立了一種依賴關係。這種關係不同於繼承的子類別/父類別關係,需要辨別這些角色,最小化他們之間的依賴關係。
還記得我們在第5章裡提到的鴨子類型嗎?鴨子類型Preparer
就是一個角色。只要實作了Preparer
介面的物件就扮演了這個角色。Mechanic
類別、Tripcoordinator
類別和Driver
類別實作了prepare_trip
方法,可以被視為Preparer
,並與其他物件互動。
許多物件導向語言都提供了某種方式,可以定義一組被命名的方法。這些方法獨立於類別,並且可以被物件使用。在 Ruby裡,這種混入內容被稱為 模組 (module) ,然後這個模組可以加入到任何物件,模組裡的方法藉由自動委派能夠被物件使用。
我們可以從物件的角度來看,雖然跟繼承有點相似,但實際上的運作是如果物件接收到無法理解的訊息,那麼這些訊息會自動轉遞到其他地方;接著,正確的方法實作會被神奇地找到,然後執行,並傳回回應。
一個包含模組的物件可回應的所有訊息包括有:
當你遇到有許多相同的程式碼時,可以思考是否要建立鴨子類型,並將共用行為放入模組,而在這之前我們需要確認物件們的行為:
假設存在有一個Schedule
類別,其介面已經包含了下面三個方法,每一個方法都帶有三個參數:實際目標,以及特定時間範圍的開始和結束日期。Schedule
負責瞭解其傳入的target
參數是否已被安排,並負責在排程表裡加入或移除target
。
scheduled?(target, starting, ending)
add(target, starting, ending)
remove(target, starting/ ending)
Schedule
自身會負責知道正確的前置時間。schedulable?
方法知道所有可能的值,並且它會檢查傳入target
參數的類別,來決定該使用哪一種前置時間。這個實作例子問題在於 Schedule
都知道太多,這些資訊不該由Schedule
提供,而應該屬於Schedule
所檢查的類別。
Schedulable
鴨子類別
將檢查類別的職責從schedulable?
方法裡移除,並且會將lead_days
訊息傳送給傳入的target
參數。
Schedule
類別不在乎target
的類別,期望target
能夠理解lead_days
,也可以說是表現得像
schedulable
的事物。
這項修改的目的是簡化程式碼,將責任推給最終物件,而不是對於類別的檢查。
讓物件自己說話
假設有一個StringUtils
類別,它實作了管理字串的實用方法。你可以向StringUtils
傳送StringUtils.empty?(some_string)
訊息,來詢問某個字串是否為空。
物件應自我管理:
圖 7.2 裡的那張順序圖違反了這項規則。發起者試圖確認target
物件是否可調度。但它不會向target
詢問 這個問題,實際上它問的是第三方(即Schedule
),這樣做會迫使發起者知道並依賴於Schedule
,這樣做會迫發起者知道並依賴於Schedule
。
先選擇一個具體的類別Bicycle
,並在該類別裡面直接實作schedulable?
方法,在做這項修改之前,每一個發起物件都必須知道Schedule
,進而形成一段依賴關係 。
class Schedule
def scheduled?(schedulable, start_date, end_date)
puts "This #{schedulable.class} " +
"is not scheduled\n" +
" between #{start_date) and #{end_date}"
false
end
end
Bicycle
知道自己的調度前置時間,並且將schedulable?
委派給了Schedule
。
class Bicycle
attr_reader :schedule, :size, :chain, :tire_size
# 注入 Schedule,並提供預設值
def initialize(args = {})
@schedule = args[:schedule] || Schedule.new
# ...
end
# 如果這輛自行車在(現在由 Bicycle 指定的)
# 這段時間內可用就傳回真
def scheedulable?(start_date, end_date)
!scheduled?(start_date - lead_days, end_date )
end
# 傳回 schedule 的回應
def scheduled?
schedule.scheduled?(self, start_date, end_date)
end
# 傳回自行車可被調度前的
# lead_days 數值
def lead_days
1
end
# ...
end
require 'date'
starting = Date.parse("2015/09/04")
ending = Date.parse("2015/09/10")
b = Bicycle.new
b.schedulable?(starting, ending)
# This Bicycle is not scheduled
# between 2015-09-03 and 2015-09-10
# => true
這段程式碼將「Schedule
是誰」以及「它做了什麼」的知識隱藏在Bicycle
裡面。 持有Bicycle
的物件不再需要知道Schedule
的存在或者其行為。
Bicycle
並不是唯一「可調度」的。Mechanic
和Vehicle
也都會扮演這個角色,因此它們也需要這個行為,現在需要重新安排這段程式碼,讓它能在不同類別之間共用。
module Schedulable
attr_writer :schedule
def schedule
@schedule ||= ::Schedule.new
end
def schedulable?(start_date, end_date)
!scheduled?(start_date - lead_days, end_date)
end
def scheduled?(start_date, end_date)
schedule.scheduled?(seif, start_date, end_date)
end
# 包含者可以加以覆蓋
def lead_days
0
end
end
上面的範例展示了一個新的Schedulable
模組。它包含一個從Bicycle
擷取出來的抽象。
增加了一個schedule
方法,對Schedule
的依賴關係已從Bicycle
裡移除,並且被移到Schedulable
模組,使其更加受到隔離。
之前Bicycle
所實作的版本會傳 回針對自行車旳數值'現在,這個模組的實作版本會傳回一個更為普遍的預設值(即 0 天)。Schedulable
模組也必須要實作lead_days
方法。針對模組的規則與針對經典繼承的規則是一樣的。如果某個模組想要傳送訊息,它必須提供實作。lead_days
方法是一個鉤子,它遵循範本方法模式。Bicycle
則覆蓋了這個鉤子(第4行),來提供自己的特殊化。
class Bicycle
include Schedulable
def lead_days
1
end
#...
end
require 'date'
starting = Date.parse("2015/09/04")
ending = rate.parse("2015/09/10")
b = Bicycle.new
b.schedulable?(starting, ending)
# This Bicycle is not scheduled
# between 2015-09-03 and 2015-09-10
# => true
schedulable?
訊息的傳送對象已從Bicycle
轉變為Schedulable
。現在你已經交給鴨子類型來處理 圖7.3則可以被調整成如圖7.4
實作Vehicle
和Mechanic
如何包括Schedulable
模組 並且回應schedulable?
訊息。
class Vehicle
include Schedulable
def lead_days
3
end
#...
end
class Mechanic
include Schedulable
def lead_days
4
end
v = Vehicle.new
v.schedulable?(starting, ending)
# This Vehicle is not scheduled
# between 2015-09-01 and 2015-09-10
# => true
m = Mechanic.new
m.schedulable?(starting, ending)
# This Mechanic is not scheduled
# between 2015-08-31 and 2015-09-10
# => true
Schedulable
裡的這段程式碼是抽象的,Schedulable
覆蓋了lead_days
,當schedulable?
抵達任何 Schedulable
時,該訊息會自動委派給這個模組中所定義的方法。
這個舉例可能並不符合嚴格的經典繼承定義,但撰寫技巧其實都是一樣的,因為都是沿著相同的路徑去尋找可 用方法。
今天又講了好大一篇,明天再接續瞭解是如何尋找相對應的方法,以及他的脈絡吧!
參考資料: